/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2009 - 2012 Luca Mingardi.
 *
 * This file is part of jeeObserver.
 *
 * JeeObserver is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * JeeObserver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package jeeobserver.server.database;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import jeeobserver.ActionCoreElement;
import jeeobserver.server.ActionParameters;
import jeeobserver.server.ActionParameters.SearchElementsParameter;
import jeeobserver.server.ActionStatistics;
import jeeobserver.server.ActionStatistics.ContextStatistics;
import jeeobserver.server.database.DatabaseFile.DatabaseEntity;
import jeeobserver.server.database.DatabaseFileElements.DatabaseElementEntity;
import jeeobserver.server.database.DatabaseFileStatistics.DatabaseStatisticsEntity;
import jeeobserver.server.ActionStatistics.Element;
import jeeobserver.server.ActionStatistics.ElementStatistics;
import jeeobserver.server.ActionStatistics.Principal;
import jeeobserver.server.ActionStatistics.PrincipalStatistics;
import jeeobserver.server.ActionStatistics.ProjectStatistics;
import jeeobserver.server.JeeObserverServerContext;
import jeeobserver.server.RequestParameter;
import jeeobserver.server.Statistics.DateStatistics;
import jeeobserver.server.Statistics.NumberStatistics;
import jeeobserver.utilities.DatabaseUtilities;

class FileActionHandler {

	private static final String ELEMENTS_FILE_NAME = "action_elements";

	private static final String STATISTICS_FILE_NAME = "action_statistics";

	private static final String PRINCIPAL_ELEMENTS_FILE_NAME = "action_principal_elements";

	private static final String PRINCIPAL_STATISTICS_FILE_NAME = "action_principal_statistics";

	private static final String EXCEPTION_ELEMENTS_FILE_NAME = "action_exception_elements";

	private static final String EXCEPTION_STATISTICS_FILE_NAME = "action_exception_statistics";

	private String path;

	private DatabaseFileElements<ActionElementEntity> fileElements;

	private DatabaseFileStatistics fileStatistics;

	private DatabaseFileElements<ActionPrincipalElementEntity> filePrincipalElements;

	private DatabaseFileStatistics filePrincipalStatistics;

	private DatabaseFileElements<ActionElementEntity> fileExceptionElements;

	private DatabaseFileStatistics fileExceptionStatistics;

	private int samplingPeriod;

	//Chached indexes
	public FileActionHandler(String path, int samplingPeriod) throws IOException {

		this.path = path;

		this.samplingPeriod = samplingPeriod;

		//Create files (if not exists)
		this.fileElements = new DatabaseFileElements<ActionElementEntity>(this.path + ELEMENTS_FILE_NAME, ActionElementEntity.class);
		this.fileStatistics = new DatabaseFileStatistics(this.path + STATISTICS_FILE_NAME, ActionStatisticsEntity.class, ActionStatisticsEntity.BYTES_SIZE);

		this.filePrincipalElements = new DatabaseFileElements<ActionPrincipalElementEntity>(this.path + PRINCIPAL_ELEMENTS_FILE_NAME, ActionPrincipalElementEntity.class);
		this.filePrincipalStatistics = new DatabaseFileStatistics(this.path + PRINCIPAL_STATISTICS_FILE_NAME, ActionStatisticsEntity.class, ActionStatisticsEntity.BYTES_SIZE);

		this.fileExceptionElements = new DatabaseFileElements<ActionElementEntity>(this.path + EXCEPTION_ELEMENTS_FILE_NAME, ActionElementEntity.class);
		this.fileExceptionStatistics = new DatabaseFileStatistics(this.path + EXCEPTION_STATISTICS_FILE_NAME, ActionStatisticsEntity.class, ActionStatisticsEntity.BYTES_SIZE);
	}

	public synchronized void persistStatistics(List<ActionCoreElement> invocationsList) throws IOException {

		if (invocationsList.isEmpty()) {
			return;
		}

		JeeObserverServerContext.logger.log(Level.FINER, "Persisting {0} Hard Disk statistics.", String.valueOf(invocationsList.size()));

		Map<Integer, DatabaseStatisticsEntity> statisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();
		Map<Integer, DatabaseElementEntity> elementsMap = new HashMap<Integer, DatabaseElementEntity>();

		Map<Integer, DatabaseStatisticsEntity> principalStatisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();
		Map<Integer, DatabaseElementEntity> principalElementsMap = new HashMap<Integer, DatabaseElementEntity>();


		//Iterate and group all invocations
		for (final ActionCoreElement coreElement : invocationsList) {

			//Find statistics sampling date
			final Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(coreElement.getTimestamp(), this.samplingPeriod);

			//Insert into cache all elements
			ActionElementEntity elementEntity = new ActionElementEntity(coreElement.getContext(), coreElement.getProject(), coreElement.getCategory(), coreElement.getName());

			if (!elementsMap.containsKey(elementEntity.getElement().getId())) {
				elementsMap.put(elementEntity.getElement().getId(), elementEntity);
			}

			//Insert into cache all statistics
			DatabaseStatisticsEntity statisticsEntity = new ActionStatisticsEntity(elementEntity.getElement().getId(), samplingDate, coreElement);

			if (!statisticsMap.containsKey(statisticsEntity.getId())) {
				statisticsMap.put(statisticsEntity.getId(), statisticsEntity);
			} else {
				statisticsMap.get(statisticsEntity.getId()).add(statisticsEntity);
			}



			//Insert into cache all principals
			if (coreElement.getPrincipal() != null) {
				ActionPrincipalElementEntity principalEntity = new ActionPrincipalElementEntity(coreElement.getPrincipal());

				if (!principalElementsMap.containsKey(principalEntity.getElement().getId())) {
					principalElementsMap.put(principalEntity.getElement().getId(), principalEntity);
				}

				//Insert into cache all principal statistics
				DatabaseStatisticsEntity principalStatisticsEntity = new ActionPrincipalStatisticsEntity(elementEntity.getElement().getId(), samplingDate, principalEntity.getElement().getId());

				if (!principalStatisticsMap.containsKey(principalStatisticsEntity.getId())) {
					principalStatisticsMap.put(principalStatisticsEntity.getId(), principalStatisticsEntity);
				} else {
					principalStatisticsMap.get(principalStatisticsEntity.getId()).add(principalStatisticsEntity);
				}
			}
		}


		//Persist cached elements and statistics into Database
		this.fileElements.merge(elementsMap);
		this.fileStatistics.persist(new HashSet<DatabaseStatisticsEntity>(statisticsMap.values()));


		//Persist cached principals and statistics into Database
		this.filePrincipalElements.merge(principalElementsMap);
		this.filePrincipalStatistics.persist(new HashSet<DatabaseStatisticsEntity>(principalStatisticsMap.values()));
	}

	public synchronized ResultSetElement<ActionStatistics.Element> searchElements(ActionParameters.SearchElementsParameter parameters) throws IOException {

		JeeObserverServerContext.logger.log(Level.FINE, "Searching Hard Disk elements. {0}", parameters.toString());

		final ResultSetElement<ActionStatistics.Element> resultList = new ResultSetElement<ActionStatistics.Element>();

		this.fileElements.reset();

		ActionElementEntity elementEntity;

		while ((elementEntity = (ActionElementEntity) this.fileElements.read()) != null) {
			//System.out.println("Filtro: " +parameters.getElement());

			if (elementEntity.passFilter(parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getCategory(), parameters.getElement())) {
				resultList.add(elementEntity.getElement());
			}
		}

		return resultList;
	}

	public synchronized ResultSetStatistics<ActionStatistics> searchStatistics(ActionParameters.SearchStatisticsParameter parameters) throws IOException {

		JeeObserverServerContext.logger.log(Level.FINE, "Searching Hard Disk statistics. {0}", parameters.toString());

		//Check groupBy parameter
		if (parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT && parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT_PROJECT && parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
			throw new IOException("GroupBy: " + parameters.getGroupBy() + " parameter not valid.");
		}

		//Search elements
		ResultSetElement<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getCategory(), parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getElement()));

		//Group by
		Map<Integer, Element> elementsGroupedElements = new HashMap<Integer, Element>();
		Map<Integer, Integer> elementsGroupedIds = new HashMap<Integer, Integer>();

		for (Element element : elements) {

			int elementGroupedId = 0;

			if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
				elementGroupedId = (element.getContext()).hashCode();
			} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
				elementGroupedId = (element.getContext() + "|" + element.getProject()).hashCode();
			} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
				elementGroupedId = element.getId();
			}

			if (!elementsGroupedIds.containsKey(element.getId())) {
				elementsGroupedIds.put(element.getId(), elementGroupedId);
			}

			if (!elementsGroupedElements.containsKey(elementGroupedId)) {
				elementsGroupedElements.put(elementGroupedId, element);
			}
		}


		Map<Date, Map<Integer, ActionStatistics>> statisticsPerPeriodMap = new HashMap<Date, Map<Integer, ActionStatistics>>();

		long startTime = System.currentTimeMillis();

		List<ActionStatisticsEntity> statisticsList = this.fileStatistics.search(elementsGroupedIds.keySet(), parameters.getDateFrom(), parameters.getDateTo());

		for (ActionStatisticsEntity statisticEntity : statisticsList) {

			//Get grouped id
			int elementGroupedId = elementsGroupedIds.get(statisticEntity.getElementId());

			//Get statistics sampling period
			Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), parameters.getSamplingPeriod());

			ActionStatistics statistics = null;

			Element element = elementsGroupedElements.get(elementGroupedId);

			if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
				statistics = new ContextStatistics(statisticEntity, element.getContext());
			} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
				statistics = new ProjectStatistics(statisticEntity, element.getContext(), element.getProject());
			} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
				statistics = new ActionStatistics.ProjectElementStatistics(statisticEntity, element.getContext(), element.getProject(), element.getName());
			}

			if (!statisticsPerPeriodMap.containsKey(samplingDate)) {
				statisticsPerPeriodMap.put(samplingDate, new HashMap<Integer, ActionStatistics>());
			}

			if (statisticsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
				statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId).add(statistics);
			} else {
				statisticsPerPeriodMap.get(samplingDate).put(elementGroupedId, statistics);
			}
		}

		long stopTime = System.currentTimeMillis();

		System.out.println("Time: " + (stopTime - startTime));

		final ResultSetStatistics<ActionStatistics> resultList = new ResultSetStatistics<ActionStatistics>(this.samplingPeriod);

		for (Date date : statisticsPerPeriodMap.keySet()) {
			Map<Integer, ActionStatistics> statisticsMap = statisticsPerPeriodMap.get(date);

			for (int id : statisticsMap.keySet()) {
				ActionStatistics statistics = statisticsMap.get(id);

				statistics.calculateTrendStatistics();

				resultList.add(statistics);
			}
		}

		return resultList;
	}

	public synchronized void deleteStatistics(ActionParameters.DeleteStatisticsParameter parameters) throws IOException {

		JeeObserverServerContext.logger.log(Level.FINE, "Deleting Hard Disk statistics. {0}", parameters.toString());

		//Filter elements
		ResultSetElement<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getCategory(), parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getElement()));

		Set<Integer> elementsSet = new HashSet<Integer>();

		for (ActionStatistics.Element element : elements) {
			elementsSet.add(element.getId());
		}



		//Delete statistics
		this.fileStatistics.delete(elementsSet, parameters.getDateFrom(), parameters.getDateTo());


		Set<Integer> elementsToNotDeleteSet = new HashSet<Integer>();

		//Delete elements
		this.fileElements.reset();

		ActionElementEntity elementEntity;

		while ((elementEntity = (ActionElementEntity) this.fileElements.read()) != null) {

			if (elementEntity.getStatus() != DatabaseElementEntity.STATUS_DELETED) {
				if (!elementsToNotDeleteSet.contains(elementEntity.getElement().getId())) {
					this.fileElements.writePervious(elementEntity);
				}
			}
		}
	}

	public synchronized void close() throws IOException {
		this.fileStatistics.close();
		this.fileElements.close();
	}

	//Action entities
	static class ActionElementEntity extends DatabaseElementEntity implements DatabaseEntity {

		private static final long serialVersionUID = 40L;

		private Element element;

		public ActionElementEntity(byte[] bytes) {
			ByteBuffer buffer = ByteBuffer.wrap(bytes);

			this.status = buffer.get();

			this.element = new Element(buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()));
		}

		public ActionElementEntity(String context, String project, String category, String name) {
			this.element = new Element(context, project, category, name);
		}

		public byte[] getBytes() {
			ByteBuffer buffer = ByteBuffer.allocate(this.getSize());

			buffer.put(this.getStatus());

			buffer.putInt(this.getElement().getId());

			buffer.putInt(this.getElement().getContext().getBytes().length);
			buffer.put(this.getElement().getContext().getBytes());

			buffer.putInt(this.getElement().getProject().getBytes().length);
			buffer.put(this.getElement().getProject().getBytes());

			buffer.putInt(this.getElement().getCategory().getBytes().length);
			buffer.put(this.getElement().getCategory().getBytes());

			buffer.putInt(this.getElement().getName().getBytes().length);
			buffer.put(this.getElement().getName().getBytes());

			return buffer.array();
		}

		public int getSize() {
			return (21 + element.getContext().getBytes().length + element.getProject().getBytes().length + element.getCategory().getBytes().length + element.getName().getBytes().length);
		}

		public boolean passFilter(Integer id, String context, String project, String category, String name) {

			if (this.status == DatabaseElementEntity.STATUS_DELETED) {
				return false;
			} else if (id != null && id.intValue() != this.element.getId()) {
				return false;
			} else if (context != null && !context.equals(this.element.getContext())) {
				return false;
			} else if (project != null && !project.equals(this.element.getProject())) {
				return false;
			} else if (category != null && !category.equals(this.element.getCategory())) {
				return false;
			} else if (name != null && !this.element.getName().contains(name)) {
				return false;
			}

			return true;
		}

		public Element getElement() {
			return element;
		}
	}

	static class ActionStatisticsEntity extends ElementStatistics implements DatabaseStatisticsEntity {

		private static final long serialVersionUID = 40L;

		public static final int BYTES_SIZE = 142;

		private transient byte status;

		public ActionStatisticsEntity(ByteBuffer buffer) {
			this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.slice());
		}

		public ActionStatisticsEntity(byte[] bytes) {
			this(ByteBuffer.wrap(bytes));
		}

		public ActionStatisticsEntity(byte status, int elementId, Date date, int count, ByteBuffer buffer) {

			super(elementId, date, count);

			this.status = status;

			this.dateStatistics = FileDatabaseHandler.getDateStatistics(buffer);

			this.cpuNanosStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
			this.timeNanosStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);

			this.executionsStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
			this.exceptionsStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
			this.usersStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
		}

		public ActionStatisticsEntity(int elementId, Date date, ActionCoreElement coreElement) {
			super(elementId, date, 1);

			this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;

			this.dateStatistics = new DateStatistics(coreElement.getTimestamp());

			this.cpuNanosStatistics = new NumberStatistics(date, coreElement.getCpuNanos());
			this.timeNanosStatistics = new NumberStatistics(date, coreElement.getTimeNanos());

			this.executionsStatistics = new NumberStatistics(date, 1);

			if (coreElement.getException() != null) {
				this.exceptionsStatistics = new NumberStatistics(date, 1);
			} else {
				this.exceptionsStatistics = new NumberStatistics(date, 0);
			}

			this.usersStatistics = new NumberStatistics(date, 0);
		}

		public byte[] getBytes() {

			ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

			buffer.put(this.status);

			buffer.putInt(this.elementId);

			FileDatabaseHandler.fillBuffer(buffer, this);

			FileDatabaseHandler.fillBuffer(buffer, this.dateStatistics);

			FileDatabaseHandler.fillBuffer(buffer, this.cpuNanosStatistics);
			FileDatabaseHandler.fillBuffer(buffer, this.timeNanosStatistics);

			FileDatabaseHandler.fillBuffer(buffer, this.executionsStatistics);
			FileDatabaseHandler.fillBuffer(buffer, this.exceptionsStatistics);
			FileDatabaseHandler.fillBuffer(buffer, this.usersStatistics);

			return buffer.array();
		}

		public int getSize() {
			return BYTES_SIZE;
		}

		public byte getStatus() {
			return status;
		}

		public void setStatus(byte status) {
			this.status = status;
		}

		public void add(DatabaseStatisticsEntity entity) {
			super.add((ActionStatisticsEntity) entity);
		}
	}

	//Action principal entities
	static class ActionPrincipalElementEntity extends DatabaseElementEntity implements DatabaseEntity {

		private static final long serialVersionUID = 40L;

		private Principal principal;

		public ActionPrincipalElementEntity(byte[] bytes) {
			ByteBuffer buffer = ByteBuffer.wrap(bytes);

			this.status = buffer.get();

			this.principal = new Principal(buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()));
		}

		public ActionPrincipalElementEntity(String principal) {
			this.principal = new Principal(principal);
		}

		public byte[] getBytes() {
			ByteBuffer buffer = ByteBuffer.allocate(this.getSize());

			buffer.put(this.getStatus());

			buffer.putInt(this.getElement().getId());

			buffer.putInt(this.getElement().getName().getBytes().length);
			buffer.put(this.getElement().getName().getBytes());

			return buffer.array();
		}

		public int getSize() {
			return (9 + principal.getName().getBytes().length);
		}

		public boolean passFilter(Integer id, String name) {

			if (this.status == DatabaseElementEntity.STATUS_DELETED) {
				return false;
			} else if (id != null && id.intValue() != this.principal.getId()) {
				return false;
			} else if (name != null && !name.equals(this.principal.getName())) {
				return false;
			}

			return true;
		}

		public Principal getElement() {
			return principal;
		}
	}

	static class ActionPrincipalStatisticsEntity extends PrincipalStatistics implements DatabaseStatisticsEntity {

		private static final long serialVersionUID = 40L;

		public static final int BYTES_SIZE = 34;

		private transient byte status;

		public ActionPrincipalStatisticsEntity(ByteBuffer buffer) {
			this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.slice());
		}

		public ActionPrincipalStatisticsEntity(byte[] bytes) {
			this(ByteBuffer.wrap(bytes));
		}

		public ActionPrincipalStatisticsEntity(byte status, int elementId, Date date, int count, ByteBuffer buffer) {

			super(elementId, date, count);

			this.status = status;

			this.principalId = buffer.getInt();

			this.usersStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
		}

		public ActionPrincipalStatisticsEntity(int elementId, Date date, int principalId) {
			super(elementId, date, 1);

			this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;

			this.principalId = principalId;

			this.usersStatistics = new NumberStatistics(date, 1);
		}

		public byte[] getBytes() {

			ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

			buffer.put(this.status);

			buffer.putInt(this.elementId);

			FileDatabaseHandler.fillBuffer(buffer, this);

			buffer.putInt(this.principalId);

			FileDatabaseHandler.fillBuffer(buffer, this.usersStatistics);

			return buffer.array();
		}

		public int getSize() {
			return BYTES_SIZE;
		}

		public byte getStatus() {
			return status;
		}

		public void setStatus(byte status) {
			this.status = status;
		}

		public void add(DatabaseStatisticsEntity entity) {
			super.add((ActionPrincipalStatisticsEntity) entity);
		}
	}
	//Action exception entities
}
